﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using System.ComponentModel;

namespace wBooks
{
    public partial class PaginatedDocument : UserControl
    {
        Dictionary<char, double> characterWidths = new Dictionary<char, double>();
        double characterHeight;
        bool isUpdating;
        int currentPageBreakIndex;

        List<int> pageBreaks = new List<int>();
        List<int> lineBreaks = new List<int>();
        public List<int> Chapters = new List<int>();

        public PaginatedDocument()
        {
            InitializeComponent();
        }

        public string ResentBook { get; set; }

        public int CurrentPage
        {
            get { return this.currentPageBreakIndex + 1; }
        }

        public int TotalPages
        {
            get { return this.pageBreaks.Count - 1; }
        }

        public string Text { get; set; }

        public int CurrentCharacterIndex { get; private set; }

        public void UpdatePagination(Action doneCallback)
        {
            if (this.Text == null || this.isUpdating)
                throw new InvalidOperationException();

            this.isUpdating = true;

            // Reset measurements
            this.pageBreaks.Clear(); this.lineBreaks.Clear();
            this.pageBreaks.Add(0); this.lineBreaks.Add(0);
            this.Chapters.Clear();
            this.characterWidths.Clear();
            this.characterHeight = -1;

            BackgroundWorker worker = new BackgroundWorker();
            worker.DoWork += delegate(object sender, DoWorkEventArgs e)
            {
                // STEP 1: BACKGROUND THREAD
                // Build up a dictionary of unique characters in the text
                for (int i = 0; i < this.Text.Length; i++)
                {
                    if (!this.characterWidths.ContainsKey(this.Text[i]))
                        this.characterWidths.Add(this.Text[i], -1);
                }

                // Copy the character keys so we can update the width values
                // without affecting the enumeration
                char[] chars = new char[this.characterWidths.Keys.Count];
                this.characterWidths.Keys.CopyTo(chars, 0);

                this.Dispatcher.BeginInvoke(delegate()
                {
                    // STEP 2: MAIN THREAD
                    // Measure the height of all characters
                    // and the width of each character

                    foreach (char c in chars)
                    {
                        // The only way to measure the width is to place the
                        // character in a text block and ask for its ActualWidth
                        this.MeasuringTextBlock.Text = c.ToString();
                        this.characterWidths[c] = this.MeasuringTextBlock.ActualWidth;

                        // The height for all characters is the same
                        // (except for newlines, which are twice the height)
                        if (this.characterHeight == -1 && !Char.IsWhiteSpace(c))
                            this.characterHeight = this.MeasuringTextBlock.ActualHeight;
                    }
                    this.MeasuringTextBlock.Text = "";

                    double pageWidth = this.Width + 1; // Allow one pixel more than width
                    double linesPerPage = this.Height / this.characterHeight;

                    BackgroundWorker worker2 = new BackgroundWorker();
                    worker2.DoWork += delegate(object sender2, DoWorkEventArgs e2)
                    {
                        // STEP 3: BACKGROUND THREAD
                        // Determine the index of each page break
                        int linesOnThisPage = 0;
                        double currentLineWidth = 0;
                        int lastWordEndingIndex = -1;

                        // Loop through each character and determine each line
                        // break based on character widths and text block wrapping behavior.
                        // A line break should then be a page break when the cumulative
                        // height of lines exceeds the page height.
                        for (int i = 0; i < this.Text.Length; i++)
                        {
                            char c = this.Text[i];

                            bool isLineBreak = false;
                            bool isForcedPageBreak = false;

                            if (c == '\n')
                            {
                                if (linesOnThisPage == 0 && currentLineWidth == 0)
                                    continue; // Skip blank lines at the start of a page

                                isLineBreak = true;
                                lastWordEndingIndex = i;
                            }
                            else if (c == '\r')
                            {
                                isLineBreak = isForcedPageBreak = true;
                                lastWordEndingIndex = i;
                                // This is the start of a chapter
                                // Add 1 because the page break isn't added yet
                                Chapters.Add(this.pageBreaks.Count + 1);
                            }
                            else
                            {
                                currentLineWidth += this.characterWidths[c];
                                // Check for a needed line break
                                if (currentLineWidth > pageWidth)
                                    isLineBreak = true;
                            }

                            if (isLineBreak)
                            {
                                linesOnThisPage++;

                                if (lastWordEndingIndex <= this.lineBreaks[this.lineBreaks.Count - 1])
                                {
                                    // The last spot where the line can be broken was already
                                    // used as a line break. Therefore, we have no choice but to
                                    // force a line break right now.
                                }
                                else
                                {
                                    // Move back to first character after the actual break, which
                                    // we may have passed due to word wrapping
                                    i = lastWordEndingIndex;
                                }

                                // Reset the width for the next line
                                currentLineWidth = 0;

                                // Skip the space between split words
                                int breakIndex;
                                if (i < this.Text.Length - 1 && this.Text[i + 1] == ' ')
                                    breakIndex = i + 1;
                                else
                                    breakIndex = i;

                                this.lineBreaks.Add(breakIndex);

                                // See if this is a page break.
                                // It is if the NEXT line would be cut off
                                bool isNaturalPageBreak = (linesOnThisPage + 1) > linesPerPage;

                                if (isForcedPageBreak || isNaturalPageBreak)
                                {
                                    this.pageBreaks.Add(breakIndex);
                                    // Reset
                                    linesOnThisPage = 0;
                                }
                            }
                            else if (c == ' ' || c == '-' || c == '—')
                                lastWordEndingIndex = i; // This can be used as a line break
                            // if we run out of space
                        }

                        // Add a final line break and page break
                        // marking the end of the document
                        if (this.lineBreaks[this.lineBreaks.Count - 1] != this.Text.Length)
                        {
                            this.lineBreaks.Add(this.Text.Length);
                            this.pageBreaks.Add(this.Text.Length);
                        }

                        // We're done!
                        doneCallback();
                        this.isUpdating = false;
                    };
                    worker2.RunWorkerAsync();
                });
            };
            worker.RunWorkerAsync();
        }

        public void ShowPageWithCharacterIndex(int characterIndex)
        {
            if (characterIndex < 0 || characterIndex >= this.Text.Length ||
                this.Text == null)
                return;

            int pageBreakIndex = this.pageBreaks.BinarySearch(characterIndex);
            if (pageBreakIndex < 0)
            {
                // The characterIndex doesn't match an exact page break, but BinarySearch
                // has returned a negative number that is the bitwise complement of the
                // index of the next element that is larger than characterIndex
                // (or the list's count if there is no larger element).
                // By subtracting one, this gives the index of the smaller element, or
                // the index of the last element if the index is too big.
                // Because 0 is in the list, this will always give a valid index.
                pageBreakIndex = ~pageBreakIndex - 1;
            }

            // If the page break index is the last one (signifying the last character
            // of the book), go back one so we'll render the whole last page
            if (pageBreakIndex == this.pageBreaks.Count - 1)
                pageBreakIndex--;

            ShowPage(pageBreakIndex + 1); // 1-based instead of 0-based
        }

        public void ShowPage(int pageNumber)
        {
            if (pageNumber >= this.pageBreaks.Count || this.Text == null)
                return;

            this.currentPageBreakIndex = pageNumber - 1;
            RefreshCurrentPage();
        }

        public void ShowPreviousPage()
        {
            if (this.currentPageBreakIndex == 0 || this.Text == null)
                return;

            this.currentPageBreakIndex--;
            RefreshCurrentPage();
        }

        public void ShowNextPage()
        {
            if (this.currentPageBreakIndex >= this.pageBreaks.Count - 2 ||
                this.Text == null)
                return;

            this.currentPageBreakIndex++;
            RefreshCurrentPage();
        }

        public void RefreshCurrentPage()
        {
            // An exact match should always be found
            int firstLineBreakIndex = this.lineBreaks.BinarySearch(
              this.pageBreaks[this.currentPageBreakIndex]);
            int lastLineBreakIndex = this.lineBreaks.BinarySearch(
              this.pageBreaks[this.currentPageBreakIndex + 1]) - 1;

            this.StackPanel.Children.Clear();

            for (int i = firstLineBreakIndex; i <= lastLineBreakIndex; i++)
            {
                // We're guaranteed that lastLineBreakIndex is always less than count - 1
                string line = this.Text.Substring(this.lineBreaks[i],
                                this.lineBreaks[i + 1] - this.lineBreaks[i]);

                line = line.Trim();

                if (line.Length == 0)
                    line = " ";

                this.StackPanel.Children.Add(new TextBlock
                {
                    Text = line,
                    Foreground = this.MeasuringTextBlock.Foreground,
                    FontSize = this.MeasuringTextBlock.FontSize,
                    FontFamily = this.MeasuringTextBlock.FontFamily
                });
            }

            this.CurrentCharacterIndex = this.lineBreaks[firstLineBreakIndex];
        }
    }
}